package org.reso.certification.codegen;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.poi.ss.usermodel.Sheet;
import org.reso.commander.common.Utils;
import org.reso.models.ReferenceStandardField;
import org.reso.models.ReferenceStandardLookup;
import org.reso.models.ReferenceStandardRelationship;
import org.w3c.dom.Document;
import org.w3c.dom.bootstrap.DOMImplementationRegistry;
import org.w3c.dom.ls.DOMImplementationLS;
import org.w3c.dom.ls.LSOutput;
import org.w3c.dom.ls.LSSerializer;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import java.io.ByteArrayInputStream;
import java.io.StringWriter;
import java.io.Writer;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import static org.reso.commander.common.DataDictionaryMetadata.v1_7.getKeyFieldForResource;
import static org.reso.commander.common.Utils.wrapColumns;

//TODO: switch to build an XML document rather than creating it as a string
public class EDMXProcessor extends WorksheetProcessor {
  private static final Logger LOG = LogManager.getLogger(EDMXProcessor.class);
  final static String EMPTY_STRING = "";
  final static String RESO_NAMESPACE = "org.reso.metadata";

  String openEntityTypeTag = null, closeEntityTypeTag = null, keyMarkup = null;

  final static String openingDataServicesTag =
      "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>"
          + "<!-- This file was autogenerated from " + REFERENCE_WORKSHEET + " on: " + Utils.getIsoTimestamp() + " -->"
          + "<edmx:Edmx xmlns:edmx=\"http://docs.oasis-open.org/odata/ns/edmx\" Version=\"4.0\">"
          + "<edmx:DataServices>";

  final static String closingDataServicesTag = "</edmx:DataServices></edmx:Edmx>";

  @Override
  public void processResourceSheet(Sheet sheet) {
    super.processResourceSheet(sheet);
    openEntityTypeTag = "<EntityType Name=\"" + sheet.getSheetName() + "\">";
    closeEntityTypeTag = "</EntityType>";
  }

  //TODO: add KeyNumeric handler
  private String getKeyMarkup(String resourceName) {
    if (resourceName == null) return null;

    String targetKeyName = getKeyFieldForResource(resourceName);

    return targetKeyName != null ?
        "<Key>"
            + "<PropertyRef Name=\"" + targetKeyName + "\" />"
            + "</Key>" : null;
  }

  @Override
  public void afterResourceSheetProcessed(Sheet sheet) {
    assert sheet != null && sheet.getSheetName() != null;
    String resourceName = sheet.getSheetName();

    String templateContent =
        openEntityTypeTag
            + getKeyMarkup(resourceName)
            + markup.toString()
            + buildNavigationPropertyMarkup(resourceName)
            + closeEntityTypeTag;

    resourceTemplates.put(resourceName, templateContent);
    resetMarkupBuffer();
  }

  @Override
  void processNumber(ReferenceStandardField field) {
    markup.append(EDMXTemplates.buildNumberMember(field));
  }

  @Override
  void processStringListSingle(ReferenceStandardField field) {
    markup.append(EDMXTemplates.buildEnumTypeSingleMember(field));
  }

  @Override
  void processString(ReferenceStandardField field) {
    markup.append(EDMXTemplates.buildStringMember(field));
  }

  @Override
  void processBoolean(ReferenceStandardField field) {
    markup.append(EDMXTemplates.buildBooleanMember(field));
  }

  @Override
  void processStringListMulti(ReferenceStandardField field) {
    markup.append(EDMXTemplates.buildEnumTypeMultiMember(field));
  }

  @Override
  void processDate(ReferenceStandardField field) {
    markup.append(EDMXTemplates.buildDateMember(field));
  }

  @Override
  void processTimestamp(ReferenceStandardField field) {
    markup.append(EDMXTemplates.buildDateTimeWithOffsetMember(field));
  }

  @Override
  void processCollection(ReferenceStandardField field) {
    LOG.debug("Collection Type is not supported at this time!");
  }

  @Override
  void generateOutput() {
    try {
      final String output =
          openingDataServicesTag
            + buildEntityTypeMarkup()
            + buildEnumTypeMarkup()
            + closingDataServicesTag;

      DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
      dbf.setValidating(false);
      DocumentBuilder db = dbf.newDocumentBuilder();
      Document document = db.parse(new ByteArrayInputStream(output.getBytes()));

      DOMImplementationRegistry registry = DOMImplementationRegistry.newInstance();
      DOMImplementationLS domImplementation = (DOMImplementationLS) registry.getDOMImplementation("LS");

      Writer stringWriter = new StringWriter();
      LSOutput formattedOutput = domImplementation.createLSOutput();
      formattedOutput.setCharacterStream(stringWriter);

      LSSerializer lsSerializer = domImplementation.createLSSerializer();
      lsSerializer.getDomConfig().setParameter("format-pretty-print", true);
      lsSerializer.getDomConfig().setParameter("xml-declaration", true);
      lsSerializer.write(document, formattedOutput);

      final String formattedOutputString = stringWriter.toString(),
          filename = getReferenceResource().replace(".xlsx", ".xml");

      LOG.info("Writing " + formattedOutputString.getBytes().length + " bytes to file: " + filename + "...");

      //write content of the string to the same directory as the source file
      Utils.createFile(getDirectoryName(), filename, stringWriter.toString());

      LOG.info("XML Metadata file written!");

    } catch (Exception ex) {
      LOG.error(ex);
    }
  }

  private String buildEntityContainerMarkup() {
    StringBuilder content = new StringBuilder();
    content.append("<EntityContainer Name=\"RESO\">");
    resourceTemplates.forEach((resourceName, templateContent) ->
        content
            .append("<EntitySet Name=\"")
            .append(resourceName).append("\" EntityType=\"" + RESO_NAMESPACE)
            .append(".")
            .append(resourceName).append("\" />"));
    content.append("</EntityContainer>");
    return content.toString();
  }

  private String buildEntityTypeMarkup() {
    StringBuilder content = new StringBuilder();
    content.append("<Schema xmlns=\"http://docs.oasis-open.org/odata/ns/edm\" Namespace=\"" + RESO_NAMESPACE + "\">");

    //iterate through each of the found resources and generate their edm:EntityType content content
    resourceTemplates.forEach((resourceName, templateContent) -> {
      content.append(templateContent);
    });

    //nest entity container in main namespace
    content.append(buildEntityContainerMarkup());

    content.append("</Schema>");
    return content.toString();
  }

  private String buildEnumTypeMarkup() {
    //enumeration markup keyed by enumeration standard name
    Map<String, String> markupMap = new LinkedHashMap<>();

    //add opening tag for enums namespace
    StringBuilder content =
        new StringBuilder("<Schema xmlns=\"http://docs.oasis-open.org/odata/ns/edm\" Namespace=\"" + RESO_NAMESPACE + ".enums\">");

    standardFieldsMap.forEach((resourceName, standardFieldMap) -> {
      standardFieldMap.forEach((standardName, referenceStandardField) -> {
        if (referenceStandardField.isSingleEnumeration()) {
          markupMap.putIfAbsent(referenceStandardField.getLookupName(), buildSingleEnumTypeMarkup(referenceStandardField));
        }

        if (referenceStandardField.isMultipleEnumeration()) {
          markupMap.putIfAbsent(referenceStandardField.getLookupName(), buildMultipleEnumTypeMarkup(referenceStandardField));
        }
      });
    });

    markupMap.forEach((lookupStandardName, markup) -> content.append(markup));

    //closing tag for enums schema definition
    content.append("</Schema>");
    return content.toString();
  }

  private String buildNavigationPropertyMarkup(String resourceName) {
    StringBuilder content = new StringBuilder();
    List<ReferenceStandardRelationship> referenceStandardRelationships =
        this.getStandardRelationships().stream().filter(referenceStandardRelationship
            -> referenceStandardRelationship.getTargetResource().contentEquals(resourceName)).collect(Collectors.toList());
    for (ReferenceStandardRelationship referenceStandardRelationship : referenceStandardRelationships) {
      //LOG.info(referenceStandardRelationship);

      if (referenceStandardRelationship.getTargetResourceKey() != null) {
        content.append("<NavigationProperty")
            .append(" Name=\"").append(referenceStandardRelationship.getTargetStandardName()).append("\"")
            .append(" Type=\"org.reso.metadata.").append(referenceStandardRelationship.getSourceResource()).append("\"")
            .append(" />");
      } else {
        content.append("<NavigationProperty")
            .append(" Name=\"").append(referenceStandardRelationship.getTargetStandardName()).append("\"")
            .append(" Type=\"Collection(org.reso.metadata.").append(referenceStandardRelationship.getSourceResource()).append(")\"")
            .append(" />");
      }
    }

    return content.toString();
  }

  private String buildSingleEnumTypeMarkup(ReferenceStandardField standardField) {
    StringBuilder content = new StringBuilder();

    if (getEnumerations().get(standardField.getLookupName()) != null) {
      content.append("<EnumType Name=\"").append(standardField.getLookupName()).append("\">");

      //iterate through each of the lookup values and generate their edm:EnumType content
      getEnumerations().get(standardField.getLookupName()).forEach(lookup -> {
        content
            .append("<Member Name=\"").append(lookup.getLookupValue()).append("\">")
            .append(EDMXTemplates.buildDisplayNameAnnotation(lookup.getLookupDisplayName()))
            .append(EDMXTemplates.buildDDWikiUrlAnnotation(lookup.getWikiPageUrl()))
            .append(EDMXTemplates.buildDescriptionAnnotation(lookup.getDefinition()))
            .append("</Member>");
      });

      content.append("</EnumType>");
    } else {
      content
          .append("<!-- TODO: implement if you are using the single-valued enumeration \"")
          .append(standardField.getLookupName()).append("\" -->")
          .append("<EnumType Name=\"").append(standardField.getLookupName()).append("\">")
          .append("<Member Name=\"Sample").append(standardField.getLookupName()).append("EnumValue").append("\"/>")
          .append("</EnumType>");
    }
    return content.toString();
  }

  private String buildMultipleEnumTypeMarkup(ReferenceStandardField standardField) {
    StringBuilder content = new StringBuilder();

    if (getEnumerations().get(standardField.getLookupName()) != null) {
      content.append("<EnumType Name=\"").append(standardField.getLookupName()).append("\">");

      //iterate through each of the lookup values and generate their edm:EnumType content
      getEnumerations().get(standardField.getLookupName()).forEach(lookup -> {
        content
            .append("<Member Name=\"").append(lookup.getLookupValue()).append("\">")
            .append(EDMXTemplates.buildDisplayNameAnnotation(lookup.getLookupDisplayName()))
            .append(EDMXTemplates.buildDDWikiUrlAnnotation(lookup.getWikiPageUrl()))
            .append(EDMXTemplates.buildDescriptionAnnotation(lookup.getDefinition()))
            .append("</Member>");
      });

      content.append("</EnumType>");
    } else {
      content
          .append("<!-- TODO: implement if you are using the multi-valued enumeration \"").append(standardField.getLookupName()).append("\" -->")
          .append("<EnumType Name=\"").append(standardField.getLookupName()).append("\">")
          .append(EDMXTemplates.buildDDWikiUrlAnnotation(standardField.getWikiPageUrl()))
          .append(EDMXTemplates.buildDescriptionAnnotation(standardField.getDefinition()))
          .append("<Member Name=\"Sample").append(standardField.getStandardName()).append("EnumValue").append("\"/>")
          .append("</EnumType>");
    }
    return content.toString();
  }

  private static String sanitizeXml(String input) {
    return input.replace("&", "&amp;")
        .replace(">", "&gt;")
        .replace("<", "&lt;")
        .replace("'", "&apos;")
        .replace("\"", "&quot;");
  }

  public static final class EDMXTemplates {
    public static String buildDisplayNameAnnotation(String content) {
      if (content == null || content.length() <= 0) return EMPTY_STRING;
      return String.format("<Annotation Term=\"RESO.OData.Metadata.StandardName\" String=\"%1$s\" />", sanitizeXml(content));
    }

    //wrapColumns(referenceStandardField.getDefinition().replaceAll("--", "-"), "   ")
    public static String buildDescriptionAnnotation(String content) {
      if (content == null || content.length() <= 0) return EMPTY_STRING;
      return String.format("<Annotation Term=\"Core.Description\" String=\"%1$s\" />", sanitizeXml(content));
    }

    public static String buildDDWikiUrlAnnotation(String content) {
      if (content == null || content.length() <= 0) return EMPTY_STRING;
      return String.format("<Annotation Term=\"RESO.DDWikiUrl\" String=\"%1$s\" />", sanitizeXml(content));
    }

    public static String buildBooleanMember(ReferenceStandardField field) {
      if (field == null) return EMPTY_STRING;
      return ""
          + "<Property Name=\"" + field.getStandardName() + "\" Type=\"Edm.Boolean\" >"
          + buildDisplayNameAnnotation(field.getDisplayName())
          + buildDDWikiUrlAnnotation(field.getWikiPageUrl())
          + buildDescriptionAnnotation(field.getDefinition())
          + "</Property>";
    }

    public static String buildDateMember(ReferenceStandardField field) {
      if (field == null) return EMPTY_STRING;
      return ""
          + "<Property Name=\"" + field.getStandardName() + "\" Type=\"Edm.Date\" >"
          + buildDisplayNameAnnotation(field.getDisplayName())
          + buildDDWikiUrlAnnotation(field.getWikiPageUrl())
          + buildDescriptionAnnotation(field.getDefinition())
          + "</Property>";
    }

    public static String buildNumberMember(ReferenceStandardField field) {
      if (field == null) return EMPTY_STRING;
      if (field.getSuggestedMaxPrecision() != null) return buildDecimalMember(field);
      else return buildIntegerMember(field);
    }

    public static String buildDecimalMember(ReferenceStandardField field) {
      if (field == null) return EMPTY_STRING;
      String template = ""
          + "<Property Name=\"" + field.getStandardName() + "\" Type=\"Edm.Decimal\"";

      //DD uses length as precision in this case
      if (field.getSuggestedMaxLength() != null) template += " Precision=\"" + field.getSuggestedMaxLength() + "\"";

      //DD uses precision as scale in this case
      if (field.getSuggestedMaxPrecision() != null) template += " Scale=\"" + field.getSuggestedMaxPrecision() + "\"";

      template += " >"
          + buildDisplayNameAnnotation(field.getDisplayName())
          + buildDDWikiUrlAnnotation(field.getWikiPageUrl())
          + buildDescriptionAnnotation(field.getDefinition())
          + "</Property>";

      return template;
    }

    public static String buildIntegerMember(ReferenceStandardField field) {
      if (field == null) return EMPTY_STRING;
      return ""
          + "<Property Name=\"" + field.getStandardName() + "\" Type=\"Edm.Int64\" >"
          + buildDisplayNameAnnotation(field.getDisplayName())
          + buildDDWikiUrlAnnotation(field.getWikiPageUrl())
          + buildDescriptionAnnotation(field.getDefinition())
          + "</Property>";
    }

    public static String buildEnumTypeSingleMember(ReferenceStandardField field) {
      if (field == null || field.getLookup() == null) return EMPTY_STRING;
      if (!field.getLookup().toLowerCase().contains("lookups")) return EMPTY_STRING;

      String lookupName = field.getLookup().replace("Lookups", "").trim();
      return ""
          + "<Property Name=\"" + field.getStandardName() + "\" Type=\"" + RESO_NAMESPACE + ".enums." + lookupName + "\" >"
          + buildDisplayNameAnnotation(field.getDisplayName())
          + buildDDWikiUrlAnnotation(field.getWikiPageUrl())
          + buildDescriptionAnnotation(field.getDefinition())
          + "</Property>";
    }

    public static String buildEnumTypeMultiMember(ReferenceStandardField field) {
      if (field == null || field.getLookup() == null) return EMPTY_STRING;
      if (!field.getLookup().toLowerCase().contains("lookups")) return EMPTY_STRING;
      return ""
          + "<Property Name=\"" + field.getStandardName()
          + "\" Type=\"Collection(" + RESO_NAMESPACE + ".enums." + field.getLookupName() + ")\">"
          + buildDisplayNameAnnotation(field.getDisplayName())
          + buildDDWikiUrlAnnotation(field.getWikiPageUrl())
          + buildDescriptionAnnotation(field.getDefinition())
          + "</Property>";

    }

    public static String buildStringMember(ReferenceStandardField field) {
      if (field == null) return EMPTY_STRING;
      String template = ""
          + "<Property Name=\"" + field.getStandardName() + "\" Type=\"Edm.String\"";

      if (field.getSuggestedMaxLength() != null) template += " MaxLength=\"" + field.getSuggestedMaxLength() + "\"";
      template += " >";

      template +=
          buildDisplayNameAnnotation(field.getDisplayName())
              + buildDDWikiUrlAnnotation(field.getWikiPageUrl())
              + buildDescriptionAnnotation(field.getDefinition())
              + "</Property>";

      return template;
    }

    public static String buildDateTimeWithOffsetMember(ReferenceStandardField field) {
      if (field == null) return EMPTY_STRING;
      String template = ""
          + "<Property Name=\"" + field.getStandardName() + "\" Type=\"Edm.DateTimeOffset\"";

      if (field.getSuggestedMaxLength() != null) template += " Precision=\"" + field.getSuggestedMaxLength() + "\"";
      template += " >";

      template +=
          buildDisplayNameAnnotation(field.getDisplayName())
              + buildDDWikiUrlAnnotation(field.getWikiPageUrl())
              + buildDescriptionAnnotation(field.getDefinition())
              + "</Property>";
      return template;
    }

    public static String buildComments(ReferenceStandardField referenceStandardField) {
      if (referenceStandardField == null || referenceStandardField.getDefinition() == null || referenceStandardField.getDefinition().length() == 0)
        return EMPTY_STRING;

      //break every COLUMN_WIDTH characters only at word boundaries
      return (referenceStandardField.getWikiPageUrl() != null && referenceStandardField.getWikiPageUrl().length() > 0 ?
          " <!-- " + referenceStandardField.getWikiPageUrl() : "")
          + " -->";
    }

    public static String buildComments(ReferenceStandardLookup referenceStandardLookup) {
      if (referenceStandardLookup == null || referenceStandardLookup.getDefinition() == null || referenceStandardLookup.getDefinition().length() == 0)
        return EMPTY_STRING;

      //break every COLUMN_WIDTH characters only at word boundaries
      return (referenceStandardLookup.getWikiPageUrl() != null && referenceStandardLookup.getWikiPageUrl().length() > 0 ? " <!-- " + referenceStandardLookup.getWikiPageUrl() : "") +
          wrapColumns(referenceStandardLookup.getDefinition().replaceAll("--", "-"), "   ")
          + " -->";
    }
  }
}